﻿using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using GalaSoft.MvvmLight.Ioc;
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Media3D;
using System.Windows.Threading;
using WorldSimulator;
using WorldSimulator.PhysicsEngine;
using WorldSimulator.RenderingEngine;
using WorldSimulatorWPF.Messages;

namespace WorldSimulatorWPF.ViewModel
{
	public class MainWindowViewModel : ViewModelBase
	{
		//// CONSTANTS

		/// <summary>
		/// Time raster the physics engine uses for calculation (in seconds)
		/// </summary>
		private const double PhysicsCalculationTimeStep = 0.03;

		//// PRIVATE FIELDS

		/// <summary>
		/// Backing field for the RenderingEngine
		/// </summary>
		private IRenderingEngine _renderingEngine = null;

		/// <summary>
		/// Backing field for the PhysicsEngine
		/// </summary>
		private IPhysicsEngine _physicsEngine = null;

		/// <summary>
		/// Backing field for the IsRunning Property
		/// </summary>
		private bool _isRunning = false;
		
		//// PUBLIC PROPERTIES

		#region DebugOutput: Beschreibung

		///<summary>
		///Beschreibung
		///</summary>
		private string _DebugOutput = string.Empty;

		///<summary>
		///Beschreibung
		///</summary>
		public string DebugOutput
		{
			get
			{
				return this._DebugOutput;
			}

			set
			{
				if (this._DebugOutput == value)
					return;

				this._DebugOutput = value;
				this.RaisePropertyChanged("DebugOutput");
			}
		}

		#endregion

		/// <summary>
		/// Tells if the simulation is initialized
		/// </summary>
		public bool IsInitialized { get; private set; }

		/// <summary>
		/// Tells the UI if there is some operation ongoing
		/// </summary>
		public bool IsBusy { get; private set; }

		/// <summary>
		/// Gets or sets the running state of the simulation
		/// </summary>
		public bool IsRunning
		{
			get
			{
				return this._isRunning;
			}

			set
			{
				if (this._isRunning == value)
					return;

				if (value == true)
				{
					new Thread(this.MainLoop).Start();
				}

				this._isRunning = value;
			}
		}

		//// CONSTRUCTION

		/// <summary>
		/// Default constructor
		/// </summary>
		public MainWindowViewModel()
		{
			this.MessengerInstance.Register<RenderingEngineConnectedMessage>(this, x => this.InitializeEngines());
		}

		//// COMMANDS

		/// <summary>
		/// Starts the simulation
		/// </summary>
		public RelayCommand StopCommand
		{
			get
			{
				return new RelayCommand(() => { this.IsRunning = false; });
			}
		}

		/// <summary>
		/// Stops the simulation
		/// </summary>
		public RelayCommand StartCommand
		{
			get
			{
				return new RelayCommand(() => { this.IsRunning = true; });
			}
		}
		
		//// PUBLIC METHODS

		/// <summary>
		/// Gets invoked when the viewmodel gets cleaned up
		/// </summary>
		public override void Cleanup()
		{
			this._isRunning = false;
			this.MessengerInstance.Unregister(this);
			base.Cleanup();
		}

		//// PROTECTED OVERWRITEABLE METHODS
		
		/// <summary>
		/// Gets called when all engines are initialized
		/// </summary>
		protected virtual void OnInitialized()
		{
			Random r = new Random();

			int particles = 200;

			for (int l = 0; l < particles; l++)
			{
				var obj = new SphereObject();
				obj.Location = new MyVector3D(0, 0, 0);
				obj.Mass = 0.01 * r.Next(10, 30);
				obj.Radius = obj.Mass;
				obj.Name = "Unnamed";
				obj.Velocity = (new MyVector3D(0.5 - r.NextDouble(), 0.5 - r.NextDouble(), 0.5 - r.NextDouble()) * 3).ToNormalized();

				this._renderingEngine.RegisterObject(obj);
				this._physicsEngine.RegisterObject(obj);
			}
		}
		
		//// PRIVATE METHODS

		/// <summary>
		/// Adds a physical object to all engines
		/// </summary>
		private void AddObject(IPhysicalObject obj)
		{
			this._renderingEngine.RegisterObject(obj);
			this._physicsEngine.RegisterObject(obj);
		}

		/// <summary>
		/// Initializes the engines (Rendering & Physics)
		/// </summary>
		private void InitializeEngines()
		{
			this.IsBusy = true;

			Task.Run(() =>
			{
				this._renderingEngine = SimpleIoc.Default.GetInstance<IRenderingEngine>();
				this._physicsEngine = SimpleIoc.Default.GetInstance<IPhysicsEngine>();

				if (this._renderingEngine == null)
					throw new NullReferenceException("RenderingEngine is not present");

				if (this._physicsEngine == null)
					throw new NullReferenceException("PhysicsEngine is not present");

				this.IsBusy = false;
				this.IsInitialized = true;

				this.OnInitialized();
			});
		}

		//// PHYSICS THREAD

		/// <summary>
		/// The last DateTime.Now.Ticks in ms
		/// </summary>
		private long _lastWorldTimeMs = 0;

		/// <summary>
		/// Returns the time in ms since the last call
		/// </summary>
		private long GetPassedMilliseconds()
		{
			long now = DateTime.Now.Ticks / 10000;
			long ret = now - this._lastWorldTimeMs;
			this._lastWorldTimeMs = now;
			return ret;
		}

		/// <summary>
		/// Main Thread for physics calculations
		/// </summary>
		private void MainLoop()
		{
			Stopwatch w = new Stopwatch();
			this.GetPassedMilliseconds();

			while (this._isRunning)
			{
				w.Reset();
				w.Start();
				this._physicsEngine.ApplyPhysics(PhysicsCalculationTimeStep);

				Thread.Sleep((int)Math.Max((PhysicsCalculationTimeStep * 1000) - this.GetPassedMilliseconds(), 0));
				this._physicsEngine.MoveObjects(PhysicsCalculationTimeStep);
				w.Stop();

				this.DebugOutput = string.Format("Physics: {0}ms", w.ElapsedMilliseconds);
				this._renderingEngine.Render();
			}
		}
	}
}